【GUI】入门 Noise(十一):迁移:如何在 CSharp 中嵌入 Racket?

#lisp/racket #gui/noise

Noise 项目在 Swift 生态中展示了极其优雅的"嵌入式后端"设计。很多 Windows 开发者可能会问:我能在 C# (.NET) 中实现同样的效果吗?

答案是肯定的。而且得益于 C# 强大的互操作性 (P/Invoke) 和 .NET 完善的异步模型,实现起来可能比 Swift 还要顺手。

本文将为你提供一份 C# 版 Noise 架构的实现蓝图,并涵盖实际开发中需要注意的关键细节。

核心挑战与解决方案

要将 Noise 的架构移植到 Windows + C#,我们需要解决三个核心问题:

  1. 加载运行时: 从 libracketcs.a (Static Lib) 变为 libracketcs.dll (Dynamic Lib)。
  2. 通信管道: 从 Unix File Descriptors 变为 Windows Handles (或 Named Pipes)。
  3. 数据传输: 在 C# 中实现 NoiseSerde 二进制协议。

第一步:加载 Racket (P/Invoke)

在 Windows 上,你需要编译 Racket CS 生成 libracketcs.dll。有了这个 DLL,我们就可以在 C# 中使用 [DllImport] 加载它。

编译 Racket CS

注意:编译 Racket CS 到 Windows DLL 需要一些额外配置:

# 在 Racket 源码目录中
# 使用 MinGW 或 Visual Studio 编译
# 需要配置 --enable-cs 编译选项
./configure --enable-cs --enable-shared
make
# 编译后会在 racket/lib 目录生成 racketcs.dll

C# 接口声明

你需要声明如下的 C 接口映射:

using System.Runtime.InteropServices;
using System.Text;

internal static class RacketRaw {
    // 确保 DLL 在运行目录或系统 PATH 中
    const string DLL = "racketcs";

    [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
    public static extern void racket_boot(ref BootArguments args);

    [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
    public static extern IntPtr racket_dynamic_require(IntPtr module, IntPtr sym);
    
    [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
    public static extern void racket_activate_thread();
    
    [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
    public static extern void racket_deactivate_thread();

    [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
    public static extern void racket_destroy();

    // 值创建辅助函数
    [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
    public static extern IntPtr racket_fixnum(long n);
    
    [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
    public static extern IntPtr racket_symbol(IntPtr s);
    
    [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
    public static extern IntPtr racket_string(IntPtr s, ulong len);
    
    [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
    public static extern IntPtr racket_cons(IntPtr a, IntPtr b);
    
    [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
    public static extern IntPtr racket_nil();

    // 字符串管理
    [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
    public static extern void racket_free(IntPtr ptr);

    [StructLayout(LayoutKind.Sequential)]
    public struct BootArguments {
        [MarshalAs(UnmanagedType.LPStr)]
        public string exec_file;
        
        [MarshalAs(UnmanagedType.LPStr)]
        public string boot1_path; // petite.boot
        
        [MarshalAs(UnmanagedType.LPStr)]
        public string boot2_path; // scheme.boot
        
        [MarshalAs(UnmanagedType.LPStr)]
        public string boot3_path; // racket.boot
    }
}

初始化逻辑

在 C# 程序启动时(或者在一个单独的 Thread 中),调用 racket_boot重要:和 Swift 版一样,初始化 Racket 的线程将成为 Racket 的"主线程",你需要一直维持这个线程存活。

public class RacketRuntime : IDisposable {
    private Thread _mainThread;
    private volatile bool _running = true;
    private bool _initialized = false;

    public void Initialize(string bootPath) {
        _mainThread = new Thread(() => {
            var args = new RacketRaw.BootArguments {
                exec_file = bootPath + "\\racket.exe",
                boot1_path = bootPath + "\\petite.boot",
                boot2_path = bootPath + "\\scheme.boot",
                boot3_path = bootPath + "\\racket.boot"
            };
            
            RacketRaw.racket_boot(ref args);
            _initialized = true;
            
            // 保持线程活跃
            while (_running) {
                Thread.Sleep(100);
            }
            
            RacketRaw.racket_destroy();
        }) {
            Name = "Racket Main Thread",
            IsBackground = false
        };
        
        _mainThread.Start();
        
        // 等待初始化完成
        while (!_initialized) {
            Thread.Sleep(10);
        }
    }

    public void Dispose() {
        _running = false;
        _mainThread?.Join(TimeSpan.FromSeconds(5));
    }
}

第二步:通信机制 (Anonymous Pipes)

这是最有趣的部分。Noise 在 Unix 上使用 pipe() 传递 fd。在 Windows 上,对应的是 Anonymous Pipes (匿名管道)。

C# 端 (Client)

using System.IO.Pipes;

public class PipePair {
    public AnonymousPipeServerStream ServerRead { get; }
    public AnonymousPipeServerStream ServerWrite { get; }
    public IntPtr ClientReadHandle { get; }
    public IntPtr ClientWriteHandle { get; }

    public PipePair() {
        // 创建两个单向管道
        // ServerWrite -> ClientRead (C# 写入,Racket 读取)
        ServerWrite = new AnonymousPipeServerStream(
            PipeDirection.Out, 
            HandleInheritability.Inheritable
        );
        ClientReadHandle = ServerWrite.ClientSafePipeHandle.DangerousGetHandle();
        
        // ServerRead <- ClientWrite (Racket 写入,C# 读取)
        ServerRead = new AnonymousPipeServerStream(
            PipeDirection.In, 
            HandleInheritability.Inheritable
        );
        ClientWriteHandle = ServerRead.GetClientHandleAsSafeHandle()
            .DangerousGetHandle();
    }
    
    public void Dispose() {
        ServerWrite?.Dispose();
        ServerRead?.Dispose();
    }
}

Racket 端 (Server)

Racket CS 在 Windows 上可以很好地处理 OS Handle,但需要特别注意文本模式与二进制模式的区别。

#lang racket/base

(require ffi/unsafe/port)

;; 将 Windows Handle 转换为 Racket Port
(define (windows-handle->port handle mode [options '(binary)])
  (unsafe-file-descriptor->port handle mode options))

;; 服务端循环
(define (serve in-handle out-handle)
  ;; 在 Windows 上必须明确指定 'binary 模式
  (define in-port (windows-handle->port in-handle 'in '(binary)))
  (define out-port (windows-handle->port out-handle 'out '(binary)))
  
  ;; 使用 input/output-bytes 进行二进制读写
  (let loop ()
    (define req-id (read-uint in-port))
    (define func-name (read-string in-port))
    (define args (read-args in-port))
    
    ;; 处理请求...
    (define result (apply func-name args))
    
    ;; 写入响应
    (write-uint out-port req-id)
    (write-result out-port result)
    (flush-output out-port)
    (loop)))

句柄传递示例

public void StartBackend() {
    var pipes = new PipePair();
    var runtime = new RacketRuntime();
    runtime.Initialize(@"C:\Program Files\Racket");
    
    // 通过 FFI 将句柄传递给 Racket
    var inHandle = RacketRaw.racket_fixnum((long)pipes.ClientReadHandle);
    var outHandle = RacketRaw.racket_fixnum((long)pipes.ClientWriteHandle);
    
    var modName = ToRacketSymbol("noise-backend");
    var serveProc = runtime.Require(modName, ToRacketSymbol("serve"));
    
    // 调用 serve(in-handle, out-handle)
    var args = RacketRaw.racket_cons(inHandle, 
                RacketRaw.racket_cons(outHandle, RacketRaw.racket_nil()));
    runtime.Call(serveProc, args);
}

private IntPtr ToRacketSymbol(string s) {
    var bytes = Encoding.UTF8.GetBytes(s);
    var ptr = Marshal.AllocHGlobal(bytes.Length + 1);
    Marshal.Copy(bytes, 0, ptr, bytes.Length);
    Marshal.WriteByte(ptr, bytes.Length, 0); // null terminator
    
    var result = RacketRaw.racket_symbol(ptr);
    RacketRaw.racket_free(ptr);
    return result;
}

第三步:数据协议 (Serialization)

NoiseSerde 的协议(Varint, Length-prefixed string)是语言无关的。在 C# 中,我们可以利用 BinaryReaderBinaryWriter 的扩展方法来实现。

Varint 实现

public static class VarintHelper {
    // ZigZag 编码:处理负数
    public static long EncodeZigZag(long n) {
        return (n << 1) ^ (n >> 63);
    }
    
    public static long DecodeZigZag(long n) {
        return (n >> 1) ^ -(n & 1);
    }

    // 读取 Varint
    public static long ReadVarint(this BinaryReader reader) {
        long result = 0;
        int shift = 0;
        byte b;
        
        do {
            b = reader.ReadByte();
            result |= (long)(b & 0x7F) << shift;
            shift += 7;
        } while ((b & 0x80) != 0);
        
        return DecodeZigZag(result);
    }

    // 写入 Varint
    public static void WriteVarint(this BinaryWriter writer, long value) {
        var encoded = EncodeZigZag(value);
        
        while (true) {
            var b = (byte)(encoded & 0x7F);
            encoded >>= 7;
            
            if (encoded == 0) {
                writer.Write(b);
                break;
            } else {
                writer.Write((byte)(b | 0x80));
            }
        }
    }
}

基础类型扩展

public static class NoiseWriterExtensions {
    public static void WriteString(this BinaryWriter w, string value) {
        var bytes = Encoding.UTF8.GetBytes(value);
        w.WriteVarint(bytes.Length);
        w.Write(bytes);
    }

    public static string ReadString(this BinaryReader r) {
        var len = r.ReadVarint();
        if (len == 0) return "";
        
        var bytes = r.ReadBytes((int)len);
        return Encoding.UTF8.GetString(bytes);
    }

    public static void WriteFloat64(this BinaryWriter w, double value) {
        var bits = BitConverter.DoubleToUInt64Bits(value);
        // Big-endian 编码(与 Swift 版保持一致)
        w.Write((byte)(bits >> 56));
        w.Write((byte)(bits >> 48));
        w.Write((byte)(bits >> 40));
        w.Write((byte)(bits >> 32));
        w.Write((byte)(bits >> 24));
        w.Write((byte)(bits >> 16));
        w.Write((byte)(bits >> 8));
        w.Write((byte)bits);
    }

    public static double ReadFloat64(this BinaryReader r) {
        var b0 = r.ReadByte();
        var b1 = r.ReadByte();
        var b2 = r.ReadByte();
        var b3 = r.ReadByte();
        var b4 = r.ReadByte();
        var b5 = r.ReadByte();
        var b6 = r.ReadByte();
        var b7 = r.ReadByte();
        
        var bits = ((ulong)b0 << 56) | 
                   ((ulong)b1 << 48) | 
                   ((ulong)b2 << 40) | 
                   ((ulong)b3 << 32) | 
                   ((ulong)b4 << 24) | 
                   ((ulong)b5 << 16) | 
                   ((ulong)b6 << 8) | 
                   (ulong)b7;
        
        return BitConverter.UInt64BitsToDouble(bits);
    }
}

第四步:异步架构 (Task & Channel)

在 C# 中,我们不需要像 Swift 那样自己实现 Future。我们有现成的 TaskCompletionSource<T>,但需要正确处理错误传播。

错误类型定义

public class RacketException : Exception {
    public string RacketErrorMessage { get; }
    
    public RacketException(string message) : base(message) {
        RacketErrorMessage = message;
    }
}

Backend 类实现

public class RacketBackend : IDisposable {
    private readonly ConcurrentDictionary<ulong, TaskCompletionSource<object>> _pending 
        = new();
    
    private readonly AnonymousPipeServerStream _reader;
    private readonly AnonymousPipeServerStream _writer;
    private readonly BinaryReader _binaryReader;
    private readonly BinaryWriter _binaryWriter;
    private readonly CancellationTokenSource _cts = new();
    private ulong _sequence = 0;
    private Task _readLoopTask;

    public RacketBackend(PipePair pipes) {
        _reader = pipes.ServerRead;
        _writer = pipes.ServerWrite;
        _binaryReader = new BinaryReader(_reader, Encoding.UTF8, leaveOpen: true);
        _binaryWriter = new BinaryWriter(_writer, Encoding.UTF8, leaveOpen: true);
        
        _readLoopTask = Task.Run(ReadLoop, _cts.Token);
    }

    private async Task ReadLoop() {
        while (!_cts.IsCancellationRequested) {
            try {
                // 1. 从管道读取 Response ID
                var id = _binaryReader.ReadVarint();
                
                // 2. 读取错误标记 (1 = 成功, 0 = 失败)
                var success = _binaryReader.ReadByte() == 1;
                
                // 3. 找到对应的 TaskCompletionSource
                if (_pending.TryRemove(id, out var tcs)) {
                    if (success) {
                        // 读取成功数据
                        var data = await ReadResultAsync(_binaryReader);
                        tcs.SetResult(data);
                    } else {
                        // 读取错误信息
                        var errorMsg = _binaryReader.ReadString();
                        tcs.SetException(new RacketException(errorMsg));
                    }
                } else {
                    // 未知响应 ID,可能是超时或已取消
                    // 仍然需要读取并丢弃数据以保持协议同步
                    await DiscardResultAsync(_binaryReader, success);
                }
            } catch (OperationCanceledException) {
                break;
            } catch (Exception ex) {
                // 记录错误但继续运行
                Console.WriteLine($"Read loop error: {ex}");
            }
        }
    }

    private async Task<object> ReadResultAsync(BinaryReader reader) {
        // 根据结果类型实现不同的读取逻辑
        // 这里简化处理,实际应该根据类型信息来决定
        return reader.ReadString(); // 示例:返回字符串
    }
    
    private async Task DiscardResultAsync(BinaryReader reader, bool success) {
        if (success) {
            await ReadResultAsync(reader);
        } else {
            reader.ReadString();
        }
    }

    public async Task<T> CallRacketAsync<T>(string funcName, params object[] args) {
        var id = _sequence++;
        var tcs = new TaskCompletionSource<object>();
        
        // 设置超时
        var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
        cts.Token.Register(() => tcs.TrySetCanceled(), false);
        
        _pending[id] = tcs;

        try {
            // 写入请求到管道
            _binaryWriter.WriteVarint(id);
            _binaryWriter.WriteString(funcName);
            
            // 写入参数
            _binaryWriter.WriteVarint(args.Length);
            foreach (var arg in args) {
                WriteArgument(_binaryWriter, arg);
            }
            
            _binaryWriter.Flush();
            
            var result = await tcs.Task;
            return (T)result;
        } catch (TaskCanceledException) {
            _pending.TryRemove(id, out _);
            throw new TimeoutException($"Racket call '{funcName}' timed out");
        } catch (Exception ex) {
            _pending.TryRemove(id, out _);
            throw;
        }
    }

    private void WriteArgument(BinaryWriter writer, object arg) {
        switch (arg) {
            case string s:
                writer.WriteVarint(0); // 类型标记
                writer.WriteString(s);
                break;
            case long l:
                writer.WriteVarint(1); // 类型标记
                writer.WriteVarint(l);
                break;
            case double d:
                writer.WriteVarint(2); // 类型标记
                writer.WriteFloat64(d);
                break;
            default:
                throw new NotSupportedException($"Type {arg.GetType()} is not supported");
        }
    }

    public void Dispose() {
        _cts.Cancel();
        _readLoopTask?.Wait(TimeSpan.FromSeconds(5));
        _cts.Dispose();
        _binaryReader?.Dispose();
        _binaryWriter?.Dispose();
        _reader?.Dispose();
        _writer?.Dispose();
    }
}

第五步:进阶——代码生成 (The "Magic")

Noise 最酷的地方在于它利用 Racket 的宏系统,根据 definition.rkt 自动生成了 Swift 代码。在 C# 中,我们应该完全复刻这一机制。

扩展 codegen.rkt

最优雅的做法是扩展 Racket 端的 codegen.rkt。查看现有的代码生成逻辑,我们可以添加 C# 代码生成器。

;; 在 codegen.rkt 中添加

(provide
 write-Swift-code
 write-CSharp-code)  ; 新增

;; C# 类型映射
(define (csharp-type t)
  ((field-type-csharp-proc t)))

(define (write-CSharp-code [out (current-output-port)])
  (fprintf out "// Generated by noise-serde-lib~n")
  (fprintf out "using System;~n")
  (fprintf out "using System.IO;~n")
  (fprintf out "using System.Text;~n~n")

  ;; 生成枚举
  (define enum-infos (get-enum-infos))
  (for ([id (in-list (sort (hash-keys enum-infos) <))])
    (define e (hash-ref enum-infos id))
    (fprintf out "~n")
    (write-CSharp-enum-code e out))

  ;; 生成记录
  (define record-infos (get-record-infos))
  (for ([id (in-list (sort (hash-keys record-infos) <))])
    (define r (hash-ref record-infos id))
    (fprintf out "~n")
    (write-CSharp-record-code r out))

  ;; 生成 Backend 类
  (fprintf out "~n")
  (write-CSharp-backend-code out))

;; C# 枚举生成
(define (write-CSharp-enum-code e [out (current-output-port)])
  (match-define (enum-info _id name _protocols variants) e)
  (fprintf out "public enum ~a {~n" name)
  
  (for ([v (in-vector variants)])
    (match-define (enum-variant _id var-name _constructor fields) v)
    (if (null? fields)
        (fprintf out "  ~a,~n" var-name)
        (fprintf out "  ~a,~n" var-name))) ; 简化处理
  
  (fprintf out "}~n"))

;; C# 记录生成
(define (write-CSharp-record-code r [out (current-output-port)])
  (match-define (record-info _id name _constructor _protocols fields) e)
  (fprintf out "public record ~a {~n" name)
  
  (for ([f (in-list fields)])
    (fprintf out "  public ~a ~a { get; init; }~n" 
             (csharp-type (record-field-type f))
             (record-field-id f)))
  
  ;; Read 方法
  (fprintf out "~n")
  (fprintf out "  public static ~a Read(BinaryReader reader) {~n" name)
  (fprintf out "    return new ~a {~n" name)
  (for ([f (in-list fields)])
    (fprintf out "      ~a = Read~a(reader),~n" 
             (record-field-id f)
             (csharp-type (record-field-type f))))
  (fprintf out "    };~n")
  (fprintf out "  }~n")
  
  ;; Write 方法
  (fprintf out "~n")
  (fprintf out "  public void Write(BinaryWriter writer) {~n")
  (for ([f (in-list fields)])
    (fprintf out "    Write~a(writer, ~a);~n" 
             (csharp-type (record-field-type f))
             (record-field-id f)))
  (fprintf out "  }~n")
  (fprintf out "}~n"))

;; C# Backend RPC 方法生成
(define (write-CSharp-backend-code out)
  (define rpc-infos (get-rpc-infos))
  
  (fprintf out "public static class RacketBackendExtensions {~n")
  
  (for ([id (in-list (sort (hash-keys rpc-infos) <))])
    (match-define (rpc-info _ rpc-name args result-type _proc)
                  (hash-ref rpc-infos id))
    
    (fprintf out "  public static async Task<~a> ~aAsync(this RacketBackend backend" 
             (csharp-type result-type)
             rpc-name)
    
    ;; 参数列表
    (for ([arg (in-list args)])
      (match-define (rpc-arg label name type) arg)
      (fprintf out ",~n    ~a ~a" (csharp-type type) name))
    
    (fprintf out ") {~n")
    (fprintf out "    return await backend.CallRacketAsync<~a>(\"~a\"" 
             (csharp-type result-type) rpc-name)
    
    ;; 传递参数
    (for ([arg (in-list args)])
      (match-define (rpc-arg _ name _) arg)
      (fprintf out ",~n      ~a" name))
    
    (fprintf out ");~n")
    (fprintf out "  }~n~n"))
  
  (fprintf out "}~n"))

类型转换器实现

;; 在 serde.rkt 中添加 C# 类型映射

(define field-type-swift-proc
  (make-field-type-proc
   '(("Float64" . "Float64")
     ("Float32" . "Float32")
     ("Int64" . "Int64")
     ("Int32" . "Int32")
     ("Int16" . "Int16")
     ("UVarint" . "UVarint")
     ("Varint" . "Varint")
     ("String" . "String")
     ("Bool" . "Bool"))))

(define field-type-csharp-proc
  (make-field-type-proc
   '(("Float64" . "double")
     ("Float32" . "float")
     ("Int64" . "long")
     ("Int32" . "int")
     ("Int16" . "short")
     ("UVarint" . "ulong")
     ("Varint" . "long")
     ("String" . "string")
     ("Bool" . "bool"))))

使用代码生成器

;; 在你的 Racket 项目中
(require noise-serde-lib/codegen)

;; 生成 C# 代码
(with-output-to-file "RacketBindings.cs"
  (λ ()
    (write-CSharp-code))
  #:exists 'replace)

第六步:性能优化与高级特性

Named Pipes 替代方案

对于高性能场景,可以考虑使用 Named Pipes:

public class NamedPipeBackend : IDisposable {
    private NamedPipeServerStream _readPipe;
    private NamedPipeServerStream _writePipe;
    
    public NamedPipeBackend(string pipeName) {
        _readPipe = new NamedPipeServerStream(
            $"{pipeName}_read",
            PipeDirection.In,
            1,
            PipeTransmissionMode.Byte,
            PipeOptions.Asynchronous
        );
        
        _writePipe = new NamedPipeServerStream(
            $"{pipeName}_write",
            PipeDirection.Out,
            1,
            PipeTransmissionMode.Byte,
            PipeOptions.Asynchronous
        );
    }
    
    // ... 类似的实现
}

内存池优化

public class PooledBuffer : IDisposable {
    private static readonly ArrayPool<byte> _pool = ArrayPool<byte>.Create(8192);
    private byte[] _buffer;
    
    public PooledBuffer(int minimumSize) {
        _buffer = _pool.Rent(minimumSize);
    }
    
    public byte[] Buffer => _buffer;
    
    public void Dispose() {
        if (_buffer != null) {
            _pool.Return(_buffer);
            _buffer = null;
        }
    }
}

总结:从 Swift 到 C#

移植 Noise 的核心思想其实就是:

  1. DLLImport 替代 Bridging Header
  2. Windows Handles 替代 File Descriptors,但要注意二进制模式。
  3. Task/Await 替代 Result/Closure,利用 .NET 强大的异步生态。
  4. Codegen:扩展 Racket 脚本,直接生成 .cs 文件,实现单一数据源。

一旦你打通了这四点,你就拥有了一个在 Windows 上运行的、基于 Racket 的超强脚本后端。无论是用于热更新逻辑、编写复杂的规则引擎,还是嵌入 DSL,这都是一个非常健壮的架构选择。